package testrank.predictors;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import testrank.Conflict;
import testrank.Predictor;
import testrank.TestRankConstants;

/**
 * @author Hagai Cibulski
 *
 */
public class ValuePropagationPredictor implements Predictor {

	private static final long serialVersionUID = 1L;
	
	protected Map<Conflict, Integer> conflicts = new HashMap<Conflict, Integer>();

	protected Map<Conflict, Integer> returnValueConflicts = new HashMap<Conflict, Integer>();
	
	protected transient Map<String, Object> lastReturnValues = new HashMap<String, Object>();
	
	protected Map<String, Integer> totalExecutionCountDuringTest = new HashMap<String, Integer>();

	protected Map<Conflict, Double> jaccardConflicts = new HashMap<Conflict, Double>();
	
	
	@Override
	public void begin(String methodSignature, String testSignature) {
	}
	@Override
	public void end(String methodSignature, String testSignature) {
	}
	public List<String> getAllTests() {
		return null;
	}
	@Override
	public String getName() {
		return "VALPROP";
	}

	@Override
	public double rankFailure(String methodSignature, String testSignature) {
		Conflict conflict = new Conflict(methodSignature, testSignature);
		Integer maxIntersection = conflicts.get(conflict);
		if (maxIntersection == null) {
			maxIntersection = 0;
		}
		
		Integer retValConflict = returnValueConflicts.get(conflict);
		if (retValConflict != null) {
			maxIntersection += retValConflict;
		}
	
		if (TestRankConstants.USE_VAL_PROP_JACCARD_NORMALIZATION) {
			// TODO consider also return value
			Double j = jaccardConflicts.get(conflict);
			return j == null ? 0 : j;
		}
		else if (TestRankConstants.USE_ALT_NORMALIZATION) {
			// normalize the conflict count by the total execution count of the test
			Integer total = totalExecutionCountDuringTest.get(testSignature);
			double rank = maxIntersection / ((double)total);
			return rank;
		}
		else {
			return (maxIntersection) / (maxIntersection + 1.0);
		}
	}

	public void begin(String methodSignature, String testSignature,
			List<Object> executionValues, List<Object> callValues) {
		ShallowSet executionSet = new ShallowSet(executionValues);
		ShallowSet callSet = new ShallowSet(callValues);
		int intersectionSize = getIntersectionSize(executionSet, callSet);
		
		Conflict conflict = new Conflict(methodSignature, testSignature);
		Integer maxIntersection = conflicts.get(conflict);
		if (maxIntersection == null || intersectionSize > maxIntersection) {
			maxIntersection = intersectionSize;
			conflicts.put(conflict, maxIntersection);
		}
		
		double jaccard = getJaccardCoefficient(executionSet, callSet); 
		Double maxJaccard = jaccardConflicts.get(conflict);
		if (maxJaccard == null || jaccard > maxJaccard) {
			maxJaccard = jaccard;
			jaccardConflicts.put(conflict, maxJaccard);
		}
		
		// update total execution count of methods in test
		Integer total = totalExecutionCountDuringTest.get(testSignature);
		if (total == null) {
			total = 0;
		}
		total += maxIntersection;
		totalExecutionCountDuringTest.put(testSignature, total);
	}

	public void end(String methodSignature, String testSignature,
			Object executionReturnValue) {
		lastReturnValues.put(methodSignature, executionReturnValue);
	}
	
	public void endCallFromTest(String testSignature, Object callReturnValue) {
		// for updating total return value intersections of methods in test
		Integer total = totalExecutionCountDuringTest.get(testSignature);
		if (total == null) {
			total = 0;
		}
		
		for (String methodSignature : lastReturnValues.keySet()) {
			Object lastRetVal = lastReturnValues.get(methodSignature);
			if (eq(lastRetVal, callReturnValue)) {
				Conflict conflict = new Conflict(methodSignature, testSignature);
				returnValueConflicts.put(conflict, 1);
				total++;
			}
		}
		
		totalExecutionCountDuringTest.put(testSignature, total);
	}
	
	public void resetLastReturnValues() {
		lastReturnValues.clear();
	}
	

	private double getJaccardCoefficient(ShallowSet executionSet, ShallowSet callSet) {
		int intersectionSize = getIntersectionSize(executionSet, callSet);
		int unionSize = getUnionSize(executionSet, callSet);
		return unionSize == 0 ? 0.0 : intersectionSize / (double)unionSize; 
	}

	private int getUnionSize(ShallowSet executionSet, ShallowSet callSet) {
		int intersectionSize = getIntersectionSize(executionSet, callSet);
		int unionSize = executionSet.size() + callSet.size() - intersectionSize;
		return unionSize;
	}
	
	private int getIntersectionSize(ShallowSet executionSet, ShallowSet callSet) {

		int count = 0;
		for (Object x : executionSet) {
			for (Object y : callSet) {
				if (eq(x, y))
					count++;
			}
		}
		return count;
	}

	private boolean eq(Object x, Object y) {
		if (x == null || y == null) {
			if (x == null && y == null) {
				return true;
			}
		} else {
			if (isEqualable(x) && isEqualable(y)) {
				if (x.equals(y))
					return true;
			}
			else if (x == y) {
				return true;
			}
		}
		return false;
	}

	private boolean isEqualable(Object x) {
		return x.getClass().isPrimitive() || 
		//x instanceof Number || // not final!
		x instanceof Byte ||
		x instanceof Double ||
		x instanceof Float ||
		x instanceof Integer ||
		x instanceof Long ||
		x instanceof Short ||
		
		x instanceof Boolean ||
		x instanceof Character ||
		x instanceof Void ||
		x instanceof String; 
	}
	
	static class ShallowSet implements Iterable<Object> {//extends AbstractSet<Object> {

		private List<Object> list = new ArrayList<Object>();
		
		public ShallowSet(List<Object> objs) {
			for (Object obj : objs) {
				if ( ! contains(obj))
					list.add(obj);
			}
		}

		public boolean contains(Object obj) {
			for (Object object : list) {
				if (obj == object)
					return true;
			}
			return false;
		}
		
		//@Override
		public Iterator<Object> iterator() {
			return list.iterator();
		}

		//@Override
		public int size() {
			return list.size();
		}
	}
}
